/*
 * MIT License
 *
 * Copyright (c) 2024-2025 milkpotatoes
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

export { };

const LOCAL_ERR_ILLEGAL_CONSTRUCTOR: typeof ERR_ILLEGAL_CONSTRUCTOR = Reflect.get(globalThis, 'ERR_ILLEGAL_CONSTRUCTOR');

declare type DOMHighResTimeStamp = number;

interface InnerEventData {
    type: string;
    target?: EventTarget;
    cancelable?: boolean;
    bubbles?: boolean;
    defaultPrevented: boolean;
    defaultPreventable: boolean;
    eventPhase: EventPhase;
    isTrusted: boolean;
    timeStamp: DOMHighResTimeStamp;
    composedPath: any[];
    immediatePropagationStoped: boolean;
    propagationStoped?: boolean;
};

const getEventTimeStamp: () => DOMHighResTimeStamp = () => {
    const t = ArkTools.timeInUs();
    return (t - (t % 5)) / 1000; // 1000 us in pre ms.
};

const innerEventsMap: WeakMap<Event, InnerEventData> = new WeakMap();

class Event {
    static get NONE(): EventPhase { return 0; };
    static get CAPTURING_PHASE(): EventPhase { return 1; };
    static get AT_TARGET(): EventPhase { return 2; };
    static get BUBBLING_PHASE(): EventPhase { return 3; };

    constructor(type: string, options?: EventInit) {
        const { cancelable = false, bubbles = false } = options ?? {};
        innerEventsMap.set(this, {
            type: String(type),
            bubbles: bubbles,
            cancelable: cancelable,
            defaultPrevented: false,
            defaultPreventable: true,
            eventPhase: Event.NONE,
            isTrusted: false,
            timeStamp: getEventTimeStamp(),
            composedPath: [],
            immediatePropagationStoped: false,
        });
    }
    // not support DOM
    get bubbles() { return innerEventsMap.get(this)!.bubbles; };
    set bubbles(_value) { };
    // not support DOM
    get composed() { return false; };
    set composed(_value) { };
    get composedPath() { return innerEventsMap.get(this)!.composedPath; };
    set composedPath(_value) { };

    get cancelable() { return innerEventsMap.get(this)!.cancelable; };
    set cancelable(_value) { };
    get currentTarget() { return innerEventsMap.get(this)!.target; };
    set currentTarget(_value) { };
    get defaultPrevented() { return innerEventsMap.get(this)!.defaultPrevented; };
    set defaultPrevented(_value) { };
    get eventPhase() { return innerEventsMap.get(this)!.eventPhase; };
    set eventPhase(_value) { };
    get isTrusted() { return innerEventsMap.get(this)!.isTrusted; };
    set isTrusted(_value) { };
    get target() { return innerEventsMap.get(this)!.target; };
    set target(_value) { };
    get type() { return innerEventsMap.get(this)!.type; };
    set type(_value) { };

    preventDefault() {
        const data = innerEventsMap.get(this)!;
        if (data.defaultPreventable) {
            data.defaultPrevented = true;
        } else {
            console.error('Unable to preventDefault inside passive event listener invocation.');
        }
    }

    stopImmediatePropagation() {
        innerEventsMap.get(this)!.immediatePropagationStoped = true;
    }
    stopPropagation() {
        innerEventsMap.get(this)!.propagationStoped = true;
    }
};

declare type EventListener = (eventt: Event) => void;

interface EventOptions {
    listener: EventListener, options: EventTargetAddInit
}

const kQueuedEventsSymbol = Symbol("queued-events");

interface EventsContainer {
    [index: string]: EventOptions[];
    [kQueuedEventsSymbol]: Event[];
}

const innerEventTargetsMap: WeakMap<EventTarget, EventsContainer> = new WeakMap();

const triggerEventListeners = (et: EventTarget) => {
    const etData = innerEventTargetsMap.get(et)!;
    const events = etData[kQueuedEventsSymbol];

    let ev = events.shift();
    while (ev) {
        const evData = innerEventsMap.get(ev)!;
        evData.target = et;
        evData.eventPhase = Event.AT_TARGET;
        const listeners = etData[evData.type];
        if (!listeners) {
            continue
        }
        const copied = [...etData[evData.type].values()];
        let i = 0;
        for (const listener of copied) {
            if (!listeners.includes(listener)) continue;
            listener.listener(ev);
            if (listener.options.once) {
                listeners.splice(i, i + 1);
                i--;
            }
            if (evData.propagationStoped) {
                break;
            }
            i++;
        }
        evData.propagationStoped = false;
        if (evData.immediatePropagationStoped) {
            break;
        }
        ev = events.shift();
    }
    etData[kQueuedEventsSymbol].slice(0, etData[kQueuedEventsSymbol].length);
}

class EventTarget {
    constructor() {
        innerEventTargetsMap.set(this, {
            [kQueuedEventsSymbol]: [],
        });
    }
    /**
     * @param useCapture {booleean} Not support DOM
     */
    addEventListener(type: string,
        listener: EventListener,
        options?: EventTargetAddInit): void {
        const data = innerEventTargetsMap.get(this)!;
        if (!data[type]) {
            data[type] = [];
        }
        const container = data[type];
        for (const data of container) {
            if (data.listener == listener) return;
        }
        if (!options) {
            container.push({ listener: listener, options: {} });
            return;
        }
        const { once = false, passive = false, signal = undefined } = options;
        if (signal?.aborted) {
            return;
        }
        signal?.addEventListener('abort', (ev: Event) => {
            ev.target?.removeEventListener(type, listener);
        });
        container.push({ listener: listener, options: options });
    }
    dispatchEvent(event: Event): boolean {
        const events = innerEventTargetsMap.get(this)!;
        const listeners = events[event.type];
        if (!listeners) {
            return true;
        }
        const evData = innerEventsMap.get(event)!;
        evData.target = this;
        events[kQueuedEventsSymbol].push(event);
        triggerEventListeners(this);
        if (innerEventsMap.get(event)!.defaultPrevented && evData.cancelable)
            return false;
        return true;
    };
    removeEventListener(type: string, listener: EventListener, options?: EventTargetRemoveInit): void {
        const events = innerEventTargetsMap.get(this)!;
        const listeners = events[type];
        if (!listeners) return;
        for (let i = listeners.length - 1; i >= 0; i--) {
            const e = listeners[i];
            if (listener === e.listener) {
                listeners.splice(i, i + 1);
            }
        }
    };
};

interface ErrorData {
    code?: number | string,
    message?: string,
}
const innerErrorDataMap: WeakMap<DOMException, ErrorData> = new WeakMap();
class DOMException extends Error {
    constructor(message?: string) {
        super(message);
        innerErrorDataMap.set(this, {});
        this.name = 'DOMException';
    }
    get code(): number | string | undefined {
        const data = innerErrorDataMap.get(this)!;
        return data.code;
    }
}

class TimeoutError extends DOMException {
    constructor() {
        super('signal timed out');
        this.name = 'TimeoutError';
        const data = innerErrorDataMap.get(this)!;
        data.code = 23;
    }
}

interface AbortSignalData {
    aborted?: boolean;
    reason?: any;
};

const innerAbortSignalMap: WeakMap<AbortSignal, AbortSignalData> = new WeakMap();

function createAbortSignal(init?: AbortSignalData): { signal: AbortSignal, data: AbortSignalData } {
    const data = init ?? { aborted: false };
    const et = new EventTarget();
    const signal = Object.setPrototypeOf(et, AbortSignal.prototype) as AbortSignal;
    innerAbortSignalMap.set(signal, data);
    signal.addEventListener('abort', (ev) => {
        const onabort = (ev.target as AbortSignal)?.onabort;
        if (onabort) {
            onabort(ev);
        }
    });
    return { signal: signal, data: data };
}

class AbortSignal extends EventTarget {
    constructor() {
        throw new LOCAL_ERR_ILLEGAL_CONSTRUCTOR('AbortSignal');
        super();
    }

    static abort(reason?: any): AbortSignal {
        return createAbortSignal({ aborted: true, reason: reason }).signal;
    }
    static any(iterable: AbortSignal[]): AbortSignal {
        const { signal, data } = createAbortSignal();
        for (let i = 0; i < iterable.length; i++) {
            const target = iterable[i];
            if (target.aborted) {
                data.aborted = true;
                data.reason = target.reason;
                return signal;
            }
        }
        for (let i = 0; i < iterable.length; i++) {
            const target = iterable[i];
            target.addEventListener('abort', ({ target: et }) => {
                if (data.aborted) {
                    return;
                }
                const st = et as AbortSignal;
                data.reason = st.reason;
                data.aborted = true;
                const event = new Event('abort');
                signal.dispatchEvent(event);
            }, { once: true });
        }
        return signal;
    }
    static timeout(time: number): AbortSignal {
        const { signal, data } = createAbortSignal({ aborted: false });
        setWeakTimeout((signal: AbortSignal, data: AbortSignalData) => {
            const ev = new Event('abort');
            data.reason = new TimeoutError();
            signal.dispatchEvent(ev);
        }, time, signal, data);
        return signal;
    }

    get aborted() { return innerAbortSignalMap.get(this)!.aborted; };
    get reason() { return innerAbortSignalMap.get(this)!.reason; };

    throwIfAborted() {
        if (innerAbortSignalMap.get(this)!.aborted) {
            Promise.reject(innerAbortSignalMap.get(this)!.reason);
        }
    }

    onabort?: EventListener;
}

const innerAbortControllerMap: WeakMap<AbortController, AbortSignal> = new WeakMap();

class AbortController {
    constructor() {
        innerAbortControllerMap.set(this, createAbortSignal().signal);
    }
    get signal(): AbortSignal {
        return innerAbortControllerMap.get(this)!;
    }
    abort(reason: any) {
        const signal = this.signal;
        const signalData = innerAbortSignalMap.get(signal)!;
        signalData.reason = reason;
        signal.dispatchEvent(new Event('abort'));
    }
}

const exports = {
    'AbortController': AbortController,
    'AbortSignal': AbortSignal,
    'DOMException': DOMException,
    'Event': Event,
    'EventTarget': EventTarget,
    'TimeoutError': TimeoutError,
} as { [index: string]: any };
for (let k in exports) {
    Object.defineProperty(globalThis, k, {
        value: exports[k]!,
        enumerable: false,
        configurable: true,
        writable: true,
    });
}
